// Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8.utils;

import com.android.tools.r8.dex.ApplicationReader.ProgramClassConflictResolver;
import com.android.tools.r8.errors.CompilationError;
import com.android.tools.r8.errors.DuplicateTypesDiagnostic;
import com.android.tools.r8.graph.ClassKind;
import com.android.tools.r8.graph.DexEncodedMethod;
import com.android.tools.r8.graph.DexProgramClass;
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.ir.desugar.BackportedMethodRewriter;
import com.android.tools.r8.ir.desugar.DesugaredLibraryWrapperSynthesizer;
import com.android.tools.r8.ir.desugar.InterfaceMethodRewriter;
import com.android.tools.r8.ir.desugar.LambdaRewriter;
import com.android.tools.r8.ir.desugar.NestBasedAccessDesugaring;
import com.android.tools.r8.ir.desugar.TwrCloseResourceRewriter;
import com.android.tools.r8.references.Reference;
import com.google.common.collect.ImmutableList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;

/** Represents a collection of library classes. */
public class ProgramClassCollection extends ClassMap<DexProgramClass> {

  private final ProgramClassConflictResolver conflictResolver;

  public static ProgramClassCollection create(
      List<DexProgramClass> classes, ProgramClassConflictResolver conflictResolver) {
    // We have all classes preloaded, but not necessarily without conflicts.
    ConcurrentHashMap<DexType, Supplier<DexProgramClass>> map = new ConcurrentHashMap<>();
    for (DexProgramClass clazz : classes) {
      map.merge(
          clazz.type, clazz, (a, b) -> conflictResolver.resolveClassConflict(a.get(), b.get()));
    }
    return new ProgramClassCollection(map, conflictResolver);
  }

  private ProgramClassCollection(
      ConcurrentHashMap<DexType, Supplier<DexProgramClass>> classes,
      ProgramClassConflictResolver conflictResolver) {
    super(classes, null);
    this.conflictResolver = conflictResolver;
  }

  @Override
  public String toString() {
    return "program classes: " + super.toString();
  }

  @Override
  DexProgramClass resolveClassConflict(DexProgramClass a, DexProgramClass b) {
    return conflictResolver.resolveClassConflict(a, b);
  }

  @Override
  Supplier<DexProgramClass> getTransparentSupplier(DexProgramClass clazz) {
    return clazz;
  }

  @Override
  ClassKind getClassKind() {
    return ClassKind.PROGRAM;
  }

  public static ProgramClassConflictResolver defaultConflictResolver(Reporter reporter) {
    // The default conflict resolver only merges synthetic classes generated by D8 correctly.
    // All other conflicts are reported as a fatal error.
    return (DexProgramClass a, DexProgramClass b) -> {
      assert a.type == b.type;
      if (a.originatesFromDexResource()
          && b.originatesFromDexResource()
          && a.accessFlags.isSynthetic()
          && b.accessFlags.isSynthetic()) {
        return mergeClasses(reporter, a, b);
      }
      throw reportDuplicateTypes(reporter, a, b);
    };
  }

  private static RuntimeException reportDuplicateTypes(
      Reporter reporter, DexProgramClass a, DexProgramClass b) {
    throw reporter.fatalError(
        new DuplicateTypesDiagnostic(
            Reference.classFromDescriptor(a.type.toDescriptorString()),
            ImmutableList.of(a.getOrigin(), b.getOrigin())));
  }

  private static DexProgramClass mergeClasses(
      Reporter reporter, DexProgramClass a, DexProgramClass b) {
    if (LambdaRewriter.hasLambdaClassPrefix(a.type)
        || BackportedMethodRewriter.hasRewrittenMethodPrefix(a.type)
        || InterfaceMethodRewriter.hasDispatchClassSuffix(a.type)
        || NestBasedAccessDesugaring.isNestConstructor(a.type)
        || TwrCloseResourceRewriter.isUtilityClassDescriptor(a.type)) {
      assert assertEqualClasses(a, b);
      return a;
    }
    if (DesugaredLibraryWrapperSynthesizer.isSynthesizedWrapper(a.type)) {
      return mergeWrappers(a, b);
    }
    throw reportDuplicateTypes(reporter, a, b);
  }

  private static boolean assertEqualClasses(DexProgramClass a, DexProgramClass b) {
    assert a.virtualMethods().size() == b.virtualMethods().size();
    assert a.directMethods().size() == b.directMethods().size();
    return true;
  }

  private static DexProgramClass mergeWrappers(DexProgramClass a, DexProgramClass b) {
    DexEncodedMethod aMethod = findConversionMethod(a);
    DexEncodedMethod bMethod = findConversionMethod(b);
    return aMethod.getCode().estimatedSizeForInlining()
            > bMethod.getCode().estimatedSizeForInlining()
        ? a
        : b;
  }

  private static DexEncodedMethod findConversionMethod(DexProgramClass clazz) {
    for (DexEncodedMethod dexEncodedMethod : clazz.directMethods()) {
      if (!dexEncodedMethod.isInstanceInitializer()) {
        return dexEncodedMethod;
      }
    }
    throw new CompilationError("A wrapper should have a conversion method.");
  }
}
